import * as Othello from './othello'
import * as Vector from "../common/vector";
import { Set } from "immutable";

const StartBoard: Othello.Cell[][] = [...Array(Othello.RowCount).keys()]
    .map(r => [...Array(Othello.ColumnCount).keys()]
        .map(c => {
            if ((c == 3 && r == 3) || (c == 4 && r == 4))
                return "Black";
            else if ((c == 3 && r == 4) || (c == 4 && r == 3))
                return "White";
            return "Empty";
        }));

class Game implements Othello.Game {
    constructor(board: Othello.Cell[][], turn: Othello.Turn, history: Othello.TurnAction[]) {
        this.CurrentBoard = board.map(r => r.slice());
        this.CurrentTurn = turn;
        this.History = history.slice();
    }

    private readonly CurrentBoard: Othello.Cell[][];
    getCurrentBoard() {
        return this.CurrentBoard.map(r => r.slice());
    }

    private readonly CurrentTurn: Othello.Turn;
    getCurrentTurn(): Othello.Turn {
        return this.CurrentTurn;
    }

    private readonly History: Othello.TurnAction[];
    getHistory() {
        return this.History.slice();
    }

    applyAction(action: Othello.TurnAction): Othello.Game | string {
        switch (this.CurrentTurn.type) {
            case "End":
                return "Cannot apply action on ended game.";
            case "Skip":
                switch (action.type) {
                    case "Movement":
                        return "Invalid movement.";
                    case "ConfirmSkip":
                        return new Game(this.CurrentBoard, Game.NextTurn(this.CurrentBoard, action.chess), this.History.slice().concat(action));
                }
                break;
            case "OnTurn":
                switch (action.type) {
                    case "ConfirmSkip":
                        return "Invalid Skip Confirm.";
                    case "Movement":
                        return this.applyMovement(action);
                }
        }

        return `Cannot to apply acion ${action.type} move on ${this.CurrentTurn.type} game`;
    }

    private static NextPlayer(current: Othello.Chess): Othello.Chess {
        return current == "White" ? "Black" : "White";
    }

    private applyMovement(movement: Othello.Movement): Othello.Game | string {
        if (this.CurrentBoard[movement.point.x][movement.point.y] != "Empty")
            return "Invalid movement";

        const cellsToFlip = Game.cells2FlipOnMove(this.CurrentBoard, movement);
        if (cellsToFlip.length == 0)
            return "Invalid movement";

        const afterBoard = this.CurrentBoard.map((r, ri) => r.map((c, ci) => {
            const current = Vector.Create(ri, ci);
            if (Vector.Equal(current, movement.point) ||
                cellsToFlip.find(v => Vector.Equal(current, v)) != undefined)
                return movement.chess;
            else
                return c;
        }));

        return new Game(afterBoard, Game.NextTurn(afterBoard, movement.chess), this.History.slice().concat(movement)
        );
    }

    private static flipCollectState(startPoint: Vector.Vector, direction: Vector.Vector) {
        return { current: startPoint, direction: direction, collected: Set<Vector.Vector>() };
    }

    private static nextFlipState(old: FlipCollectState): FlipCollectState {
        return {
            current: Vector.Add(old.current, old.direction),
            direction: old.direction,
            collected: old.collected.add(old.current)
        };
    }

    private static AllDirUnits = [[1, 0], [0, 1], [-1, 0], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]].map(arr => Vector.Create(arr[0], arr[1]));

    private static cells2FlipOnMove(board: Othello.Cell[][], movement: Othello.Movement): Vector.Vector[] {
        const isMovePoint = (v: Vector.Vector) => Vector.Equal(v, movement.point);
        const nextToMovePoint = (p: Vector.Vector, dir: Vector.Vector) => Vector.Equal(Vector.Add(movement.point, dir), p);
        const outOfBoard = (p: Vector.Vector) => !Othello.InBoard(p);

        const collectDir: (s: FlipCollectState) => Set<Vector.Vector> =
            state => {
                if (isMovePoint(state.current))
                    return collectDir({
                        current: Vector.Add(state.current, state.direction),
                        direction: state.direction,
                        collected: state.collected
                    });
                else if (outOfBoard(state.current))
                    return Set<Vector.Vector>();
                else if (nextToMovePoint(state.current, state.direction)) {
                    const cell = board[state.current.x][state.current.y];
                    if (cell != "Empty" && cell != movement.chess)
                        return collectDir(Game.nextFlipState(state));
                    else
                        return Set<Vector.Vector>();
                }
                else {
                    const cell = board[state.current.x][state.current.y];
                    if (cell == "Empty")
                        return Set<Vector.Vector>();
                    else if (cell == movement.chess)
                        return state.collected;
                    else
                        return collectDir(Game.nextFlipState(state));
                }
            };

        return Game.AllDirUnits
            .map(dir => Game.flipCollectState(movement.point, dir))
            .map(collectDir)
            .reduce((result, dir) => result.union(dir), Set<Vector.Vector>())
            .toArray();
    }

    private static NextTurn(board: Othello.Cell[][], currentTurnChess: Othello.Chess): Othello.Turn {
        const ntChess = Game.NextPlayer(currentTurnChess);
        if (Game.AnyValidMovePoint(board, ntChess))
            return { type: "OnTurn", chess: ntChess }
        else if (Game.AnyValidMovePoint(board, currentTurnChess))
            return { type: "Skip", chess: ntChess };
        else {
            const cells = board.reduce((l, r) => l.concat(r));
            const cCount = cells.filter(x => x == currentTurnChess).length;
            const nCount = cells.filter(x => x == ntChess).length;

            return cCount == nCount ?
                { type: "End", chess: currentTurnChess, winner: "Draw" }
                : { type: "End", chess: currentTurnChess, winner: cCount > nCount ? currentTurnChess : ntChess };
        }
    }

    private static AnyValidMovePoint(board: Othello.Cell[][], chess: Othello.Chess) {
        return board
            .map((r, ri) => r.map((c, ci) => c == "Empty" ? Vector.Create(ri, ci) : undefined))
            .reduce((l, r) => l.concat(r))
            .filter(v => v != undefined)
            .find((v: Vector.Vector) => Game.cells2FlipOnMove(board, { type: "Movement", chess: chess, point: v }).length != 0)
            != undefined;
    }
}

interface FlipCollectState {
    current: Vector.Vector,
    direction: Vector.Vector,
    collected: Set<Vector.Vector>
}

export const NewGame: Othello.Game = new Game(
    StartBoard,
    {
        type: "OnTurn",
        chess: "White"
    },
    []);
